#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author：albert time:2021/9/2


"""
2048游戏
本模块已完整实现2048游戏的算法及分数的计算算法
本游戏的界面采用python 标准库 tkinter 来实现
此界面的布局采用tkinter中的grid布局
此模块是单独模块，通过此模块直接可以运行
"""

import random  # 导入随机模块random,主要用于随机生成新的数字及数字摆方位置
import math  # 导入数学模块,用来计算分数
from TimeManager import TimeManager


from tkinter import *
from tkinter import messagebox
import tkinter as tk


# MapData 绑定一个 4 x 4 列表,此列表为2048游戏地图，初始值如下:
MapData = [
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]
]

# 背景颜色
game_bg_color = "#bbada0"


# 游戏中每个数据对应色块的颜色
MapColor = {
    0: ("#cdc1b4", "#776e65"),
    2: ("#eee4da", "#776e65"),
    4: ("#ede0c8", "#f9f6f2"),
    8: ("#f2b179", "#f9f6f2"),
    16: ("#f59563", "#f9f6f2"),
    32: ("#f67c5f", "#f9f6f2"),
    64: ("#f65e3b", "#f9f6f2"),
    128: ("#edcf72", "#f9f6f2"),
    256: ("#edcc61", "#f9f6f2"),
    512: ("#e4c02a", "#f9f6f2"),
    1024: ("#e2ba13", "#f9f6f2"),
    2048: ("#ecc400", "#f9f6f2"),
    4096: ("#ae84a8", "#f9f6f2"),
    8192: ("#b06ca8", "#f9f6f2"),
    # ----其它颜色都与8192相同---------
    2**14: ("#b06ca8", "#f9f6f2"),
    2**15: ("#b06ca8", "#f9f6f2"),
    2**16: ("#b06ca8", "#f9f6f2"),
    2**17: ("#b06ca8", "#f9f6f2"),
    2**18: ("#b06ca8", "#f9f6f2"),
    2**19: ("#b06ca8", "#f9f6f2"),
    2**20: ("#b06ca8", "#f9f6f2"),
}


class Game2048_time:
    """
    2048游戏计时模式类
    """

    def __init__(self):
        """
        初始化数据
        """
        self.gesture = ''
        # 判断窗口状态字段
        self.active = True
        self.root = None
        self._map_data = None
        self.keymap = None
        self.frame = None
        self.label = None
        self.label_score = None
        self.label_time = None
        self.number = None
        self.restart_button = None
        self.map_labels = None
        self.time_manager = None
    @staticmethod
    def _new_tkinter_obj():
        """
        生成tkinter窗口对象
        :return tkinter窗口对象
        """
        # 创建tkinter窗口
        root = Tk()
        # 设置标题文字
        root.title('2048(手势识别的2048)')
        # 固定宽和高
        root.resizable(width=False, height=False)
        return root

    def _new_frame(self):
        """
        生成frame窗口对象
        :return Frame窗口对象
        """
        # 创建一个frame窗口，此创建将容纳全部的widget 部件
        frame = Frame(self.root, bg=game_bg_color)
        frame.grid(sticky=N + E + W + S)
        # 设置焦点能接收按键事件
        frame.focus_set()
        frame.bind("<Key>", self.on_key_down)
        return frame

    def on_key_down(self, event):
        """
        键盘按下处理函数
        """
        keysym = event.keysym
        if keysym in self.keymap:
            if self.keymap[keysym]():  # 如果有数字移动
                self.fill2()  # 填充一个新的2
        self.update_ui()
        if self.is_game_over():
            mb = messagebox.askyesno(
                title="gameover", message="游戏结束!\n是否退出游戏!")
            if mb:
                self.root.quit()
            else:
                self.reset()
                self.update_ui()

    def gesture_handle(self, direction):
        """
        处理手势
        :param direction: str 方向 'up' 'down' 'right' 'left'
        """


        move_flag = False
        if direction == 'up':
            move_flag = self.up()
        elif direction == 'down':
            move_flag = self.down()
        elif direction == 'left':
            move_flag = self.left()
        elif direction == 'right':
            move_flag = self.right()
        # elif direction == 'fist':
        #
        if move_flag:
            # 填充一个新的2
            self.fill2()
        self.update_ui()
        if self.is_game_over():
            mb = messagebox.askyesno(
                title="GameOver", message="游戏结束!\n是否退出游戏!")
            if mb:
                self.root.quit()
            else:
                self.reset()
                self.update_ui()
    def get_map_labels(self):
        """
        获取map_labels
        """
        # 初始化图形界面
        # 创建4x4的数字块
        map_labels = []  # 游戏各方块的lable Widget
        for r in range(4):
            row = []
            for c in range(len(self._map_data[0])):
                value = self._map_data[r][c]
                text = str(value) if value else ''
                self.label = Label(self.frame, text=text, width=4, height=2,
                                   font=("黑体", 30, "bold"))
                self.label.grid(row=r, column=c, padx=5, pady=5, sticky=N + E + W + S)
                row.append(self.label)
            map_labels.append(row)
        return map_labels

    def set_label_button(self):
        """
        设置显示分数的Label和重新开始按钮
        """
        # 设置显示分数的Lable
        self.label = Label(self.frame, text='分数', font=("黑体", 30, "bold"),
                           bg="#bbada0", fg="#eee4da")
        self.label.grid(row=4, column=0, padx=5, pady=5)
        self.label_score = Label(self.frame, text='0', font=("黑体", 30, "bold"),
                                 bg="#bbada0", fg="#ffffff")
        self.label_score.grid(row=4, column=1, padx=5, pady=5)
        self.label_time = Label(self.frame, text='0', font=("黑体", 30, "bold"),
                                 bg="#bbada0", fg="#ffffff")
        self.label_time.grid(row=4, column=2, padx=5, pady=5)
        self.restart_button = Button(self.frame, text='重新开始', font=("黑体", 16, "bold"),
                                     bg="#8f7a66", fg="#f9f6f2", command=self.reset_game)
        self.restart_button.grid(row=4, column=3, padx=5, pady=5)

    def reset_game(self):
        """
        重新开始游戏
        """
        self.reset()
        self.update_ui()
        self.time_manager.reset_and_start_timer()


    def reset(self):
        """
        重新设置游戏数据,将地图恢复为初始状态，并加入两个数据 2 作用初始状态
        """
        # _map_data.clear()
        self._map_data[:] = []
        self._map_data.append([0, 0, 0, 0])
        self._map_data.append([0, 0, 0, 0])
        self._map_data.append([0, 0, 0, 0])
        self._map_data.append([0, 0, 0, 0])
        # 在空白地图上填充两个2
        self.fill2()
        self.fill2()


    def get_space_count(self):
        """
        获取没有数字的方格的数量,如果数量为0则说有无法填充新数据，游戏即将结束
        """
        count = 0
        for r in self._map_data:
            count += r.count(0)
        return count

    def get_score(self):
        """
        获取游戏的分数,得分规则是每次有两个数加在一起则生成相应的分数。
        如 2 和 2 合并后得4分, 8 和 8 分并后得 16分.
        根据一个大于2的数字就可以知道他共合并了多少次，可以直接算出分数:
        如:
           4 一定由两个2合并，得4分
           8 一定由两个4合并,则计:8 + 4 + 4 得32分
           ... 以此类推
        """
        score = 0
        for r in self._map_data:
            for c in r:
                score += 0 if c < 4 else c * int((math.log(c, 2) - 1.0))
        return score  # 导入数学模块

    def fill2(self):
        """
        填充2到空位置，如果填度成功返回True,如果已满，则返回False
        """
        blank_count = self.get_space_count()  # 得到地图上空白位置的个数
        if 0 == blank_count:
            return False
        # 生成随机位置, 如，当只有四个空时，则生成0~3的数，代表自左至右，自上而下的空位置
        pos = random.randrange(0, blank_count)
        offset = 0
        for row in self._map_data:   # row为行row
            for col in range(4):  # col 为列，column
                if 0 == row[col]:
                    if offset == pos:
                        # 把2填充到第row行，第col列的位置，返回True
                        row[col] = 2
                        return True
                    offset += 1

    def is_game_over(self):
        """
        判断游戏是否结束,如果结束返回True,否是返回False
        """
        for r in self._map_data:
            # 如果水平方向还有0,则游戏没有结束
            if r.count(0):
                return False
            # 水平方向如果有两个相邻的元素相同，应当是可以合并的，则游戏没有结束
            for i in range(3):
                if r[i] == r[i + 1]:
                    return False
        for c in range(4):
            # 竖直方向如果有两个相邻的元素相同，应当可以合并的，则游戏没有结束
            for r in range(3):
                if self._map_data[r][c] == self._map_data[r + 1][c]:
                    return False
        # 以上都没有，则游戏结束
        return True

    # 以下是2048游戏的基本算法,此种算法不是最优算法,但我认为这是目前最容易理解的算法
    @staticmethod
    def _left_move_number(line):
        """
        左移一行数字,如果有数据移动则返回True，否则返回False:
        如: line = [0, 2, 0, 8] 即表达如下一行:
            +---+---+---+---+
            | 0 | 2 | 0 | 8 |      <----向左移动
            +---+---+---+---+
        此行数据需要左移三次:
          第一次左移结果:
            +---+---+---+---+
            | 2 | 0 | 8 | 0 |
            +---+---+---+---+
          第二次左移结果:
            +---+---+---+---+
            | 2 | 8 | 0 | 0 |
            +---+---+---+---+
          第三次左移结果:
            +---+---+---+---+
            | 2 | 8 | 0 | 0 |  # 因为最左则为2,所以8不动
            +---+---+---+---+
         最终结果: line = [4, 8, 0, 0]
        """
        move_flag = False  # 是否移动的标识,先假设没有移动
        for _ in range(3):  # 重复执行下面算法三次
            for i in range(3):  # i为索引
                if 0 == line[i]:  # 此处有空位，右侧相邻数字向左侧移动，右侧填空白
                    move_flag = True
                    line[i] = line[i + 1]
                    line[i + 1] = 0
        return move_flag

    @staticmethod
    def _left_merge_number(line):
        """
        向左侧进行相同单元格合并,合并结果放在左侧,右侧补零
        如: line = [2, 2, 4, 4] 即表达如下一行:
            +---+---+---+---+
            | 2 | 2 | 4 | 4 |
            +---+---+---+---+
        全并后的结果为:
            +---+---+---+---+
            | 4 | 0 | 8 | 0 |
            +---+---+---+---+
        最终结果: line = [4, 8, 8, 0]
        """
        for i in range(3):
            if line[i] == line[i + 1]:
                line[i] *= 2  # 左侧翻倍
                line[i + 1] = 0  # 右侧归零

    def _left_move_aline(self, line):
        """
        左移一行数据,如果有数据移动则返回True，否则返回False:
        如: line = [2, 0, 2, 8] 即表达如下一行:
            +---+---+---+---+
            | 2 |   | 2 | 8 |      <----向左移动
            +---+---+---+---+
        左移算法分为三步:
            1. 将所有数字向左移动来填补左侧空格,即:
                +---+---+---+---+
                | 2 | 2 | 8 |   |
                +---+---+---+---+
            2. 判断是否发生碰幢，如果两个相临且相等的数值则说明有碰撞需要合并,
               合并结果靠左，右则填充空格
                +---+---+---+---+
                | 4 |   | 8 |   |
                +---+---+---+---+
            3. 再重复第一步，将所有数字向左移动来填补左侧空格,即:
                +---+---+---+---+
                | 4 | 8 |   |   |
                +---+---+---+---+
            最终结果: line = [4, 8, 0, 0]
        """
        move_flag = False
        if self._left_move_number(line):
            move_flag = True
        if self._left_merge_number(line):
            move_flag = True
        if self._left_move_number(line):
            move_flag = True
        return move_flag

    def left(self):
        """
        游戏左键按下时或向左滑动屏幕时的算法
        """
        move_flag = False  # move_flag 是否成功移动数字标志位,如果有移动则为真值,原地图不变则为假值
        # 将第一行都向左移动.如果有移动就返回True
        for line in self._map_data:
            if self._left_move_aline(line):
                move_flag = True
        return move_flag

    def right(self):
        """
        游戏右键按下时或向右滑动屏幕时的算法
        选将屏幕进行左右对调，对调后，原来的向右滑动即为现在的向左滑动
        滑动完毕后，再次左右对调回来
        """
        # 左右对调
        for r in self._map_data:
            r.reverse()
        move_flag = self.left()  # 向左滑动
        # 再次左右对调
        for r in self._map_data:
            r.reverse()
        return move_flag

    def up(self):
        """
        游戏上键按下时或向上滑动屏幕时的算法
        先把每一列都自上而下放入一个列表中line中，然后执行向滑动，
        滑动完成后再将新位置摆回到原来的一列中
        """
        move_flag = False
        for col in range(4):  # 先取出每一列
            # 把一列中的每一行数入放入到line中
            line = [0, 0, 0, 0]  # 先初始化一行，准备放入数据
            for row in range(4):
                line[row] = self._map_data[row][col]
            # 将当前列进行上移，即line 左移
            if self._left_move_aline(line):
                move_flag = True
            # 把左移后的 line中的数据填充回原来的一列
            for row in range(4):
                self._map_data[row][col] = line[row]
        return move_flag

    def down(self):
        """
        游戏下键按下时或向下滑动屏幕时的算法
        选将屏幕进行上下对调，对调后，原来的向下滑动即为现在的向上滑动
        滑动完毕后，再次上下对调回来
        """
        self._map_data.reverse()
        move_flag = self.up()  # 上滑
        self._map_data.reverse()
        return move_flag

    def update_ui(self):
        """
        刷新界面函数
        根据计算出的f地图数据,更新各个Label的设置
        """
        for r in range(4):
            for c in range(len(self._map_data[0])):
                self.number = self._map_data[r][c]  # 设置数字
                self.label = self.map_labels[r][c]  # 选中Lable控件
                self.label['text'] = str(self.number) if self.number else ''
                self.label['bg'] = MapColor[self.number][0]
                self.label['foreground'] = MapColor[self.number][1]
        self.label_score['text'] = str(self.get_score())  # 重设置分数

    def quit(self):
        """
        点击窗口关闭按钮
        """
        elapsed_time = self.label_time['text']
        if messagebox.askyesno('2048',  f'你确定要退出游戏吗?\n'
                                        f'你的分数是 {self.get_score()}, 已玩时间 {elapsed_time}'):
            self.root.destroy()
            self.root.quit()
            # if messagebox.askyesno('2048', '你确定要退出游戏吗?'):





    def run(self):
        """
        需要在此函数初始化tkinter对象，因为tkinter对象和mainloop方法必须处于同一线程
        """
        # 初始化tkinter对象
        self.root = self._new_tkinter_obj()
        # 设置退出释放tcl解释器
        self.root.protocol("WM_DELETE_WINDOW", self.quit)
        self._map_data = MapData
        # keymap不删除，可以同时保留键盘操作
        self.keymap = {
            'a': self.left,
            'd': self.right,
            'w': self.up,
            's': self.down,
            'Left': self.left,
            'Right': self.right,
            'Up': self.up,
            'Down': self.down,
            'q': self.root.quit,
        }
        # 初始化frame对象
        self.frame = self._new_frame()
        # 获取map_labels
        self.map_labels = self.get_map_labels()
        # 设置显示分数的Label和重新开始按钮
        self.set_label_button()
        self.fill2()
        self.fill2()
        self.time_manager = TimeManager(self.root, self.label_time)
        self.time_manager.start_timer()
        # 更新界面
        self.update_ui()
        # 进入tkinter主事件循环
        self.root.mainloop()
        # 关闭窗口，修改状态
        self.active = False


